/*
 * Author: Rolando Fentanes
 * Class: StorageController
 * Provides a link from logic to storage
 */

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.io.StreamException;
import com.thoughtworks.xstream.io.xml.DomDriver;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedList;

public class Storage {
    private XStream xstream;
    private String filename = "data.xml";
    private File f = new File(filename);
    private Data data;

    // class that holds all data to be saved to file
    class Data {
        ArrayList<Player> users;
        ArrayList<Game> games;

        Data() {
            users = new ArrayList();
            games = new ArrayList();
        }
    }

    // Compares Players with just Strings, the String representing the username
    class NameComparator implements Comparator {
         public int compare(Object emp1, Object emp2) {
             String emp1Name = ((Player)emp1).getName();
             String emp2Name = ((String)emp2);

             return emp1Name.compareTo(emp2Name);
         }
    }

    // compares a Game with a list of Players, Players in String format
    class PlayersComparator implements Comparator {
         public int compare(Object emp1, Object emp2) {
             int result = 0;
             String game1player, game2player;

             // sort Games by list of usernames
             for(int i = 0; i < Math.min(((Game)emp1).sortedplayers.size(), ((ArrayList<String>)emp2).size()); i++) {
                 game1player = ((Game)emp1).sortedplayers.get(i).getName();
                 game2player = ((ArrayList<String>)emp2).get(i);
                 result = game1player.compareTo(game2player);
                 if(result != 0)
                     return result;
             }

             return result;
         }
    }

    // comparator for sorting Games by Players and then by last Date saved
    class GameComparator implements Comparator {
         public int compare(Object emp1, Object emp2) {
             int result = 0;
             Player game1player, game2player;

             // sort Games by list of usernames
             for(int i = 0; i < Math.min(((Game)emp1).sortedplayers.size(), ((Game)emp2).sortedplayers.size()); i++) {
                 game1player = ((Game)emp1).sortedplayers.get(i);
                 game2player = ((Game)emp2).sortedplayers.get(i);
                 result = game1player.compareTo(game2player);
                 if(result != 0)
                     return result;
             }

             // if all users are the same, sort it by date
             return ((Game)emp1).lastSaved.compareTo(((Game)emp2).lastSaved);
         }
    }

    // verifies data from xml file matches up
    class GameVerify implements Verify {
        public Boolean verify(Object obj) {
            Game game = (Game)obj;
            try {
                // first verify the users in the list of players exist and is equal to list of sorted players
                for(Player player: game.players) {
                    // Check if user exists in user list
                    // Not sure if this should be checked because if the users in game
                    // are altered then what's stopping someone from editing the actual list of users?
                    /*
                    if(authenticateUser(player.getName()) == null)
                        return false;
                     */

                    // Check if users in players are in sortedplayers
                    // Both list must have the same players, just sorted differently
                    if(Collections.binarySearch(game.sortedplayers, player) < 0) {
                        System.out.println("STORAGE: Player " + player.getName() + " in Game.players not found in Game.sortedplayers.");
                        return false;
                    }
                }

                // Check if first player is in player list
                if(Collections.binarySearch(game.sortedplayers, game.first) < 0) {
                    System.out.println("STORAGE: Player " + game.first.getName() + " in Game.first not found in Game.sortedplayers.");
                    return false;
                }

                // Initializing Rules..
                game.checkers = new Rules(game.boards);
                game.checkers.setCurrentPlayer(game.currentPlayer);

                // check boards in Game
                if(!checkBoards(game.boards, game.sortedplayers, game.checkers, "Game.boards"))
                    return false;

                // check MovesLog
                for(int i = 0; i < game.gameMoves.moveList.size(); i++) {
                    for(Move move: game.gameMoves.moveList.get(i)) {
                        if(move.locations.size() == 0) {
                            System.out.println("STORAGE: Move contains no Locations.");
                            return false;
                        }
                        try {
                            for(Location loc: move.locations) {
                                int dummy = game.boards.get(loc.board).Positions[loc.x][loc.y].size();
                            }
                        } catch(IndexOutOfBoundsException ex) {
                            System.out.println("STORAGE: Move Location is out of bounds.");
                            return false;
                        }
                    }
                }

                // check initialBoards of MovesLog
                if(!checkBoards(game.gameMoves.initialBoards, game.sortedplayers, game.checkers, "Game.MovesLog.initialBoards"))
                    return false;


                // if this gets this far, this Game object is aaallright
                return true;
                
            } catch(NullPointerException ex) {
                // one of the variables that needed to be checked is null
                System.out.println("STORAGE: " + ex.getMessage());
                return false;
            }
        }
        
        Boolean checkBoards(ArrayList<Board> boards, ArrayList<Player> sortedplayers, Rules rules, String varName) {
            if(boards.size() == 0) {
                System.out.println("STORAGE: " + varName + ": Does not contain any Boards.");
                return false;
            }

            // reset Pieces in Rules
            rules.setPieces(sortedplayers);

            for(Board board: boards) {
                if(board.size.width < 8 || board.size.height < 8) {
                    System.out.println("STORAGE: " + varName + ": Board size cannot be less than 8x8.");
                    return false;
                }
                
                // might have something else to verify Pieces later
                // but I want them to be flexible too
                // i've added this... but it's temporary imo
                try {
                    for(int c = 0; c < board.size.width; c++)
                        for(int r = 0; r < board.size.height; r++) {
                            for(Piece piece: board.Positions[c][r]) {
                                if(Collections.binarySearch(sortedplayers, piece.owner, new NameComparator()) < 0) {
                                    System.out.println("STORAGE: " + varName + ": The owner " + piece.owner + " of Piece is not in Game.sortedplayers.");
                                    return false;
                                }
                                String err = rules.validateAddition(piece.location, piece);
                                if(err != null) {
                                    System.out.println("STORAGE: " + varName + ": " + err);
                                    return false;
                                }
                            }
                        }
                } catch(IndexOutOfBoundsException ex) {
                    // in case size doesn't match actual size of Positions
                    System.out.println("STORAGE: " + varName + ": Board size does not match actual Position dimensions.");
                    return false;
                }
            }
            
            return true;
        }
    }

    // Constructor
    Storage() {
        xstream = new XStream(new DomDriver());

        xstream.omitField(Data.class, "this$0");
        xstream.omitField(ArrayList.class, "this$0");
        xstream.omitField(LinkedList.class, "this$0");
        
        xstream.omitField(Player.class, "this$0");
        xstream.omitField(Statistics.class, "this$0");
        
        xstream.omitField(Game.class, "this$0");
        xstream.omitField(Game.class, "checkers");
        xstream.omitField(Piece.class, "this$0");
        xstream.omitField(Board.class, "this$0");
        xstream.omitField(Move.class, "this$0");
        xstream.omitField(MovesLog.class, "this$0");

        data = new Data();
    }


    // ************** USER METHODS ****************

    // checks if user with passed usename exists and returns password for comparison
    String authenticateUser(String username) {
        loadData();

        try {
            return data.users.get(Collections.binarySearch(data.users, username, new NameComparator())).getPassword();
        } catch (IndexOutOfBoundsException ex) {
            // User not found
            return null;
        }
    }

    // returns list of users
    ArrayList<String> getUsers() {
        loadData();

        ArrayList<String> usernames = new ArrayList();
        // copy list of usernames over to new ArrayList
        for(Player player: data.users)
            usernames.add(player.getName());

        return usernames;
    }

    // checks if username with passed username exists and returns Player object
    Player getUser(String username) {
        loadData();

        try {
            return data.users.get(Collections.binarySearch(data.users, username, new NameComparator()));
        } catch (IndexOutOfBoundsException ex) {
            // User not found
            return null;
        }
    }

    // create a new user by adding it to the list of users
    // cannot add a duplicate user
    void createUser(Player user) {
        loadData();

        // add user to list
        int i = Collections.binarySearch(data.users, user);
        try {
            data.users.add(-i-1, user);
            saveData();
        } catch (IndexOutOfBoundsException ex) {
            // User is already on the list
            // update reference
            user = data.users.get(i);
        }
    }

    // deletes a user account from memory
    void deleteUser(String username) {
        try {
            loadData();

            // remove user from list of users
            data.users.remove(Collections.binarySearch(data.users, username, new NameComparator()));

            // search for Games where that user participated in
            for(int i = data.games.size()-1; i >= 0 ; i--) {
                int k = Collections.binarySearch(data.games.get(i).sortedplayers, username, new NameComparator());
                if(k >= 0)
                    // player was found in list of players and so we remove the Game
                    data.games.remove(i);
            }

            saveData();
        } catch (IndexOutOfBoundsException ex) {
            // user is not found
        }
    }


    // ************** STATISTICS METHODS ****************

    // deletes Stats of a Player
    void clearUserStats(String username) {
        try {
            loadData();

            // creates new instance of Statistic in Player
            data.users.get(Collections.binarySearch(data.users, username, new NameComparator())).stats = new Statistics();

            saveData();
        } catch (IndexOutOfBoundsException ex) {
            // user is not found
        }
    }

    // resets Stats of every Player
    void clearAllStats() {
        loadData();

        // go through all users creating new Statistics in each of them
        for(Player player: data.users)
            player.stats = new Statistics();

        // save users
        saveData();
    }

    // returns a list of Statistics of a user
    ArrayList<Integer> getUserStats(String username) {
        try {
            loadData();

            return data.users.get(Collections.binarySearch(data.users, username, new NameComparator())).stats.getStats();
        } catch (IndexOutOfBoundsException ex) {
            // user is not found
            return null;
        }
    }

    // returns list of players a player has played with
    ArrayList<String> getPlayers(Player player) {
        ArrayList<String> players =  new ArrayList();

        // search list of games
        for(Game game: data.games) {
            int index = Collections.binarySearch(game.sortedplayers, player);
            int size = players.size();
            if(index >= 0) {
                // add users from one list to another
                // making sure not to add duplicates
                for(Player p: game.sortedplayers) {
                    int index2 = Collections.binarySearch(players, p.getName());
                    if(index2 < 0)
                        players.add(p.getName());
                    else
                        index--;
                }

                // remove original player from list
                players.remove(size+index);
            }
        }

        return players;
    }


    // ************** GAMES METHODS ****************

    // gets list of saved games that players have played and saved
    ArrayList<Date> getGames(ArrayList<String> players) {
        loadData();

        ArrayList<Date> list = new ArrayList();
        ArrayList<String> sortedplayers = (ArrayList<String>) players.clone();
        Collections.sort(sortedplayers);

        // search through list of saved Games and compare list of players
        int i = Collections.binarySearch(data.games, sortedplayers, new PlayersComparator());
        if(i >= 0 && data.games.get(i).finished == false) {
            list.add(data.games.get(i).lastSaved);
            int k;

            // keep searching.. there may be more games with these players
            k = i;
            while(k < data.games.size()-1 && new PlayersComparator().compare(data.games.get(++k), sortedplayers) == 0 && data.games.get(k).finished == false)
                list.add(data.games.get(k).lastSaved);
            k = i;
            while(k > 0 && new PlayersComparator().compare(data.games.get(--k), sortedplayers) == 0 && data.games.get(k).finished == false)
                list.add(data.games.get(k).lastSaved);
        }

        return list;
    }

    // returns Game info from one Game
    Game getGame(Date date, ArrayList<String> players) {
        loadData();

        // create a dummy Game object to use for searching list of Games
        Game dummy = new Game();
        dummy.lastSaved = date;
        dummy.sortedplayers = new ArrayList();
        for(String player: players) {
            int i = Collections.binarySearch(dummy.sortedplayers, player, new NameComparator());
            dummy.sortedplayers.add(-i-1, new Player(player));
        }

        int i = Collections.binarySearch(data.games, dummy, new GameComparator());
        if(i >= 0 && data.games.get(i).finished == false) {
            return data.games.get(i);
        }
        else
            // game either not found, is already in use,  or finished, so return null
            return null;
    }

    // saves a Game
    void saveGame(Game game) {
        loadData();

        // search list of games
        int i = Collections.binarySearch(data.games, game, new GameComparator());
        if(i >= 0) {
            // Game was found
            // update latest save data
            game.lastSaved = new Date();

            // resort list of Games since date was updated
            Collections.sort(data.games, new GameComparator());
            saveData();
            return;
        }

        // sort as we add game to list
        i = Collections.binarySearch(data.games, game, new GameComparator());
        data.games.add(-i-1, game);

        saveData();
    }

    // **************** ACCESSING DATA FILES *******************

    private void loadData() {
        FileInputStream input = null;

        try {
            // open file
            input = new FileInputStream(f);

            // read data and unserialize
            Data newData = (Data)xstream.fromXML(input);
            input.close();

            // update data
            CopyList.copyList(data.users, newData.users);
            CopyList.copyList(data.games, newData.games, new GameComparator (), new GameVerify());

        } catch(FileNotFoundException ex) {
            try {
                // file does not exist so create it
                System.out.println("STORAGE: " + filename + " not found. Creating file.");
                f.createNewFile();
            } catch (IOException ex2) {

            }
        } catch(StreamException ex) {
            System.out.println("STORAGE: Problem unserializing data. Data in file either does not match or is corrupted.");
        } catch(IOException ex) {

        } catch(ConversionException ex) {
            // file is corrupted
            System.out.println("STORAGE: Problem unserializing data. Data in file either does not match or is corrupted.");
        } finally {
            if(input != null)
                try {
                    input.close();
                } catch (IOException ex) {

                }
        }
    }

    // save list of games to file
    void saveData() {
        FileOutputStream output = null;

        try {
            output = new FileOutputStream(f);
            // serialize data to xml format and try to write to file
            xstream.toXML(data, output);
        } catch (FileNotFoundException ex) {
            try {
                // file does not exist so create it
                // this might happen if for some reason someone erased the file while the game was running
                System.out.println("STORAGE: " + filename + " not found. Creating file.");
                f.createNewFile();
                // try to save games again
                saveData();
            } catch (IOException ex2) {

            }
        } catch (IOException ex) {

        } finally {
            if(output != null)
                try {
                    output.close();
                } catch (IOException ex) {

                }
        }
    }
}
